Frolian's blog

xkcd, Python, math and beyond

The seven modes of Tetris

In this post, our goal is to transpose Tetris to all seven modes of the major scale and play it with the help of the Game Boy sound that we have developed in a previous post.

For those not familiar with the major scale and its modes, I will not attempt an explication here. Please look it up on Wikipedia as it is a complicated notion.

Transposing to a different mode

First, we're going to define the modes of the major scale we will use.

In [1]:
from collections import OrderedDict

modes = OrderedDict()
for mode_name, alterations in zip(['lydian', 'ionian', 'mixolydian', 'dorian', 'aeolian', 'phrygian', 'locrian'],
                                 [[0, 0, 0, -1, 0, 0, 0],
                                  [0, 0, 0, 0, 0, 0, -1],
                                  [0, 0, -1, 0, 0, 0, 0],
                                  [0, 0, 0, 0, 0, -1, 0],
                                  [0, -1, 0, 0, 0, 0, 0],
                                  [0, 0, 0, 0, -1, 0, 0],
                                  [0, 1, 1, 1, 1, 1, 1]]):
    modes[mode_name] = alterations

Then, we define the melody (Tetris in our case) in the Nokia melody format.

In [2]:
tetris = "4e6,8b5,8c6,8d6,16e6,16d6,8c6,8b5,4a5,8a5,8c6,4e6,8d6,8c6,4b5,8b5,8c6,4d6,4e6,4c6,4a5,2a5,8p,4d6,8f6,4a6,8g6,8f6,4e6,8e6,8c6,4e6,8d6,8c6,4b5,8b5,8c6,4d6,4e6,4c6,4a5,4a5"

Now, we import the tools we will use.

In [3]:
from pylab import *
from scipy.signal import square

Let's process the melody step by step. Our test case is that we want to go from aeolian, in which the melody is written, to phrygian. This means that only one note changes: b becomes a#.

In [4]:
key = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
starting_mode = 'aeolian'
ending_mode = 'dorian'
In [5]:
mode_names = modes.keys()
start_index = mode_names.index(starting_mode)
end_index = mode_names.index(ending_mode)
if end_index < start_index:
    end_index += 7
In [6]:
print start_index, end_index
4 10
In [7]:
transposition = array([0, 0, 0, 0, 0, 0, 0])
for i in range(start_index, end_index):
    transposition += array(modes[mode_names[i % 7]])
print transposition

note_scale = ["a", "a#", "b", "c", "c#", "d", "d#", "e", "f", "f#", "g", "g#"]
transposed_melody =  []
for note in tetris.split(','):
    if note[-1] == 'p':
        transposed_melody.append(note)
    else:
        for target_note in note_scale:
            if note.find(target_note) != -1:
                duration, octave = note.split(target_note)
                break
        transposed_melody.append(
            duration + note_scale[(note_scale.index(target_note) + transposition[key.index(target_note)]) % 12] + octave)
",".join(transposed_melody)
[0 0 0 0 0 1 0]
Out[7]:
'4e6,8b5,8c6,8d6,16e6,16d6,8c6,8b5,4a5,8a5,8c6,4e6,8d6,8c6,4b5,8b5,8c6,4d6,4e6,4c6,4a5,2a5,8p,4d6,8f#6,4a6,8g6,8f#6,4e6,8e6,8c6,4e6,8d6,8c6,4b5,8b5,8c6,4d6,4e6,4c6,4a5,4a5'
In [8]:
def transpose(melody, key, starting_mode, ending_mode):
    mode_names = modes.keys()
    start_index = mode_names.index(starting_mode)
    end_index = mode_names.index(ending_mode)
    if end_index < start_index:
        end_index += 7
    transposition = array([0, 0, 0, 0, 0, 0, 0])
    for i in range(start_index, end_index):
        transposition += array(modes[mode_names[i % 7]])
    #print transposition
    note_scale = ["a", "a#", "b", "c", "c#", "d", "d#", "e", "f", "f#", "g", "g#"]
    transposed_melody =  []
    for note in melody.split(','):
        if note[-1] == 'p':
            transposed_melody.append(note)
        else:
            for target_note in note_scale:
                if note.find(target_note) != -1:
                    duration, octave = note.split(target_note)
                    break
            transposed_melody.append(
                duration + note_scale[(note_scale.index(target_note) + transposition[key.index(target_note)]) % 12] + octave)
    return ",".join(transposed_melody)
In [9]:
tetris
Out[9]:
'4e6,8b5,8c6,8d6,16e6,16d6,8c6,8b5,4a5,8a5,8c6,4e6,8d6,8c6,4b5,8b5,8c6,4d6,4e6,4c6,4a5,2a5,8p,4d6,8f6,4a6,8g6,8f6,4e6,8e6,8c6,4e6,8d6,8c6,4b5,8b5,8c6,4d6,4e6,4c6,4a5,4a5'
In [10]:
transpose(tetris, ['a', 'b', 'c', 'd', 'e', 'f', 'g'], 'aeolian', 'dorian')
Out[10]:
'4e6,8b5,8c6,8d6,16e6,16d6,8c6,8b5,4a5,8a5,8c6,4e6,8d6,8c6,4b5,8b5,8c6,4d6,4e6,4c6,4a5,2a5,8p,4d6,8f#6,4a6,8g6,8f#6,4e6,8e6,8c6,4e6,8d6,8c6,4b5,8b5,8c6,4d6,4e6,4c6,4a5,4a5'

Playing the transposed melody with a Game Boy sound

From our previous post, we know how to play this sort of ringtone with the code below:

In [11]:
import re
from IPython.display import Audio, display
In [12]:
def play_melody(melody, sample_freq=10.e3, bpm=50):
    duration = re.compile("^[0-9]+")
    pitch = re.compile("[\D]+[\d]*") 
    measure_duration = 4 * 60. / bpm #usually it's 4/4 measures
    output = zeros((0,))
    for note in melody.split(','):
        # regexp matching
        duration_match = duration.findall(note)
        pitch_match = pitch.findall(note)
        
        # duration 
        if len(duration_match) == 0:
            t_max = 1/4.
        else:
            t_max = 1/float(duration_match[0])
        if "." in pitch_match[0]:
            t_max *= 1.5
            pitch_match[0] = "".join(pitch_match[0].split("."))
        t_max = t_max * measure_duration
        
        # pitch
        if pitch_match[0] == 'p':
            freq = 0
        else:
            if pitch_match[0][-1] in ["4", "5", "6", "7"]: # octave is known
                octave = ["4", "5", "6", "7"].index(pitch_match[0][-1]) + 4 
                height = pitch_match[0][:-1]
            else: # octave is not known
                octave = 5
                height = pitch_match[0]
            freq = 261.626 * 2 ** ((["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"].index(height) / 12. + octave - 4))  
            
        # generate sound
        t = arange(0, t_max, 1/sample_freq)
        wave = square(2 * pi * freq * t)
        
        # append to output
        output = hstack((output, wave))
    
    display(Audio(output, rate=sample_freq)) 

Using this rudimentary appartus, we can now listen to what this sounds like:

In [13]:
from IPython.html.widgets import interact, fixed
In [14]:
def play_transposed_melody(mode): 
    transposed_melody = transpose(tetris, ['a', 'b', 'c', 'd', 'e', 'f', 'g'], 'aeolian', mode)
    #print transposed_melody
    play_melody(transposed_melody, bpm=130)

interact(play_transposed_melody,
         mode=modes.keys())
        
Out[14]:
<function __main__.play_transposed_melody>

For rendering purposes, we're outputting the 7 modes below:

In [15]:
for mode in modes.keys():
    print mode
    play_transposed_melody(mode)
lydian
ionian
mixolydian
dorian
aeolian
phrygian
locrian

Bonus: pitch-shifting the result with the bowl sound from Zulko

First, we copy Zulko's code for pitch-shifting.

In [16]:
import numpy as np

def speedx(snd_array, factor):
    """ Multiplies the sound's speed by some `factor` """
    indices = np.round( np.arange(0, len(snd_array), factor) )
    indices = indices[indices < len(snd_array)].astype(int)
    return snd_array[ indices.astype(int) ]
In [17]:
def stretch(sound_array, f, window_size, h):
    """ Stretches the sound by a factor `f` """

    phase  = np.zeros(window_size)
    hanning_window = np.hanning(window_size)
    result = np.zeros( len(sound_array) /f + window_size)

    for i in np.arange(0, len(sound_array)-(window_size+h), h*f):

        # two potentially overlapping subarrays
        a1 = sound_array[i: i + window_size]
        a2 = sound_array[i + h: i + window_size + h]

        # resynchronize the second array on the first
        s1 =  np.fft.fft(hanning_window * a1)
        s2 =  np.fft.fft(hanning_window * a2)
        phase = (phase + np.angle(s2/s1)) % 2*np.pi
        a2_rephased = np.fft.ifft(np.abs(s2)*np.exp(1j*phase))

        # add to result
        i2 = int(i/f)
        result[i2 : i2 + window_size] += hanning_window*a2_rephased

    result = ((2**(16-4)) * result/result.max()) # normalize (16bit)

    return result.astype('int16')
In [18]:
def pitchshift(snd_array, n, window_size=2**13, h=2**11):
    """ Changes the pitch of a sound by ``n`` semitones. """
    factor = 2**(1.0 * n / 12.0)
    stretched = stretch(snd_array, 1.0/factor, window_size, h)
    return speedx(stretched[window_size:], factor)

Now, we generate the sounds we're going to need:

In [19]:
from scipy.io import wavfile

fps, bowl_sound = wavfile.read("../../../Pianoputer/bowl.wav")
tones = range(-25,25)
transposed = [pitchshift(bowl_sound, n) for n in tones]
-c:22: ComplexWarning: Casting complex values to real discards the imaginary part
In [20]:
print fps
48000

These sounds can be plugged in the note generation process.

In [23]:
def play_melody_with_bowl(melody, sample_freq=10.e3, bpm=50):
    duration = re.compile("^[0-9]+")
    pitch = re.compile("[\D]+[\d]*") 
    measure_duration = 4 * 60. / bpm #usually it's 4/4 measures
    output = zeros((0,))
    for note in melody.split(','):
        # regexp matching
        duration_match = duration.findall(note)
        pitch_match = pitch.findall(note)
        
        # duration 
        if len(duration_match) == 0:
            t_max = 1/4.
        else:
            t_max = 1/float(duration_match[0])
        if "." in pitch_match[0]:
            t_max *= 1.5
            pitch_match[0] = "".join(pitch_match[0].split("."))
        t_max = t_max * measure_duration
        
        # pitch
        if pitch_match[0] == 'p':
            freq = 0
        else:
            if pitch_match[0][-1] in ["4", "5", "6", "7"]: # octave is known
                octave = ["4", "5", "6", "7"].index(pitch_match[0][-1]) + 4 
                height = pitch_match[0][:-1]
            else: # octave is not known
                octave = 5
                height = pitch_match[0]
            sound_index = (["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"].index(height) + (octave - 5) * 12) 
        
        # generate sound
        t = arange(0, t_max, 1./sample_freq)
        wave = transposed[sound_index]
        wave = wave[:t.size]
        
        # append to output
        output = hstack((output, wave))
    
    display(Audio(output, rate=sample_freq)) 

A simple test below will show us if our program works:

In [24]:
play_melody_with_bowl(transpose(tetris, ['a', 'b', 'c', 'd', 'e', 'f', 'g'], 'aeolian', 'aeolian'), sample_freq=fps, bpm=120.)

And now, to finish this post, please enjoy the seven modes playing Tetris with a bowl sound courtesy of the amazing Zulko's blog:

In [25]:
for mode in modes.keys():
    print mode
    play_melody_with_bowl(transpose(tetris, ['a', 'b', 'c', 'd', 'e', 'f', 'g'], 'aeolian', mode), sample_freq=fps, bpm=120.)
lydian
ionian
mixolydian
dorian
aeolian
phrygian
locrian

This post was entirely written using the IPython notebook. You can see a static view or download this notebook with the help of nbviewer at 20140917_SevenModesOfTetris.ipynb.

Comments